import java.util.*;

/**
 * Class to simulate a process scheduler.
 * CS 149 Project, Phase 1
 * @author Keven Wong
 * @author Timmy Yee
 */

public class Scheduler
{
    private static Algorithm algorithm = Algorithm.ROUND_ROBIN;
    private static Queue<Process> newQ;
    private static Queue<Process> readyQ;
    private static Queue<Process> diskQ;
    private static Queue<Process> printerQ;
    private static Queue<Process> finishQ;

    /**
     * Checks whether a new process has been put in the new queue.
     * @param time - absolute current time
     * @return true if new process is in new queue, false otherwise
     */
    private static boolean hasNewProcessArrived(double time)
    {
        if (newQ.peek() == null)
            return false;

        return Util.areDoublesEqual(time, newQ.peek().getArrivalTime());
    }

    /**
     * Creates a process from new queue and puts it into ready queue.
     * @param time - absolute current time
     * @param gui - the panel containing the trace
     */
    private static void createProcess(double time, Display gui)
    {
        Process p = newQ.poll();
        p.setCreationTime(time);
        offerToReadyQ(p, time);
        gui.appendLog(time, "New process (" + p.getID() + ") arrives\n");
    }

    /**
     * Puts new process to the ready queue
     * @param p - new process
     * @param time - absolute current time
     */
    public static void offerToReadyQ(Process p, double time)
    {
        p.setEnterTime(time);
        readyQ.offer(p);
    }

    /**
     * Removes a process from the ready queue.
     * @param time - absolute current time
     * @return process removed from ready queue
     */
    public static Process pollFromReadyQ(double time)
    {
        Process p = readyQ.poll();

        if (p != null)
            p.setLeaveTime(time);

        return p;
    }

    /**
     * Initializes all of the process queues.
     */
    public static void initQueues()
    {
        newQ = new PriorityQueue<Process>(11, new Comparator<Process>() {
            public int compare(Process p1, Process p2) {
                if (p1.getArrivalTime() < p2.getArrivalTime())
                    return -1;
                else if (p1.getArrivalTime() > p2.getArrivalTime())
                    return 1;
                else
                    return 0;
            }
        });

        if (algorithm == Algorithm.ROUND_ROBIN)
            readyQ = new LinkedList<Process>();
        else // if priority scheduling
            readyQ = new PriorityQueue<Process>(11, new Comparator<Process>() {
                public int compare(Process p1, Process p2) {
                    if (p1.getPriority() < p2.getPriority())
                        return -1;
                    else if (p1.getPriority() > p2.getPriority())
                        return 1;
                    else
                        return 0;
                }
            });

        diskQ = new LinkedList<Process>();
        printerQ = new LinkedList<Process>();
        finishQ = new LinkedList<Process>();
    }

    /**
     * Sets the process scheduling algorithm to use.
     * @param a - process scheduling algorithm to use
     */
    public static void setAlgorithm(Algorithm a)
    {
        algorithm = a;
    }

    /**
     * Gets the process scheduling algorithm currently in use.
     * @return current process scheduling algorithm in use
     */
    public static Algorithm getAlgorithm()
    {
        return algorithm;
    }

    /**
     * Gets the name of a process scheduling algorithm.
     * @return name of a process scheduling algorithm
     */
    public static String getAlgorithmName()
    {
        if (algorithm == Algorithm.PREEMPT)
            return "Preemptive Priority";
        else if (algorithm == Algorithm.NON_PREEMPT)
            return "Non-preemptive Priority";
        else if (algorithm == Algorithm.ROUND_ROBIN)
            return "Round Robin";
        else
            return "";
    }

    /**
     * Adds a new process to new queue.
     * @param p - process to add to new queue
     */
    public static void addNew(Process p)
    {
        newQ.offer(p);
    }

    /**
     * Checks if there are any processes in any of the queues.
     * @return true if processes are in any queues, false otherwise
     */
    public static boolean areProcessesActive()
    {
        return !(newQ.isEmpty() && readyQ.isEmpty() &&
                 diskQ.isEmpty() && printerQ.isEmpty() &&
                 Processor.hasNoProcess());
    }

    /**
     * Creates a new process if a process arrives in new queue.
     * @param time - absolute current time
     * @param gui - the panel containing the trace
     */
    public static void checkForNewArrival(double time, Display gui)
    {
        if (hasNewProcessArrived(time))
            createProcess(time, gui);
    }

    /**
     * Moves a process making a disk request into disk queue.
     * @param p - process making disk request
     * @param time - absolute current time
     * @param gui - the panel containing the trace
     */
    public static void assignToDisk(Process p, double time, Display gui)
    {
        p.startDiskOperation();
        diskQ.offer(p);

        gui.appendLog(time, "Trap instruction (disk system call) occurred\n");
        gui.appendLog(time, "Process " + p.getID() +
                            " requests disk operation\n");
        gui.appendLog(time, "Process " + p.getID() +
                            " added to Disk Queue\n");
    }

    /**
     * Moves a process out of the disk queue if finished with disk request.
     * @param time - absolute current time
     * @return process to move out of disk queue
     */
    public static Process getProcessFromDiskQ(double time)
    {
        if (diskQ.peek() == null)
            return null;

        Process p = diskQ.peek();
        p.useDevice();
        if (Util.areDoublesEqual(p.getDeviceTime(), 0))
            return diskQ.poll();
        else
            return null;
    }

    /**
     * Moves a process making a print request into printer queue.
     * @param p - process making print request
     * @param time - absolute current time
     * @param gui - the panel containing the trace
     */
    public static void assignToPrinter(Process p, double time, Display gui)
    {
        p.startPrintOperation();
        printerQ.offer(p);

        gui.appendLog(time, "Trap instruction (print system call) occurred\n");
        gui.appendLog(time, "Process " + p.getID() +
                            " requests print operation\n");
        gui.appendLog(time, "Process " + p.getID() +
                            " added to Printer Queue\n");
    }

    /**
     * Moves a process out of printer queue if finished with print request.
     * @param time - absolute current time
     * @return process to move out of printer queue
     */
    public static Process getProcessFromPrinterQ(double time)
    {
        if (printerQ.peek() == null)
            return null;

        Process p = printerQ.peek();
        p.useDevice();
        if (Util.areDoublesEqual(p.getDeviceTime(), 0))
            return printerQ.poll();
        else
            return null;
    }

    /**
     * Swaps running processes.
     * @param time - absolute current time
     * @param gui - the panel containing the trace
     */
    public static void swapProcesses(double time, Display gui)
    {
        Process old = Processor.getCurrent();

        if (old.isDone())
            terminateProcess(old, time, gui);
        else
            offerToReadyQ(old, time);

        Processor.assignProcess(time, gui);
    }

    /**
     * Terminates a process.
     * @param p - process to terminate
     * @param time - absolute current time
     * @param gui - the panel containing the trace
     */
    public static void terminateProcess(Process p, double time, Display gui)
    {
        p.setTurnaroundTime(time);
        finishQ.offer(p);
        gui.appendLog(time, "Process " + p.getID() + " terminated\n");
    }

    /**
     * Gets the contents of all of the queues.
     * @return the contents of all queues
     */
    public static String getQueuesContent()
    {
        String content = "Queues content (remaining CPU time)\n";

        content += "\tReady Q: ";
        for (Process p : readyQ)
            content += String.format("%d(%.1f), ",
                                     p.getID(), p.getProcessingTime());
        content += "\n";

        content += "\tDisk Q: ";
        for (Process p : diskQ)
            content += String.format("%d(%.1f), ",
                                     p.getID(), p.getProcessingTime());
        content += "\n";

        content += "\tPrinter Q: ";
        for (Process p : printerQ)
            content += String.format("%d(%.1f), ",
                                     p.getID(), p.getProcessingTime());
        content += "\n";

        return content;
    }

    /**
     * Gets the average turnaround time of all of the processes.
     * @return average turnaround time of all processes
     */
    public static double getAvgTurnaround()
    {
        double sum = 0;

        for (Process p : finishQ)
            sum += p.getTurnaroundTime();

        return sum / finishQ.size();
    }

    /**
     * Gets the average waiting time of all of the processes.
     * @return average waiting time of all processes
     */
    public static double getAvgWaiting()
    {
        double sum = 0;

        for (Process p : finishQ)
            sum += p.getWaitingTime();

        return sum / finishQ.size();
    }

    /**
     * Gets the average response time of all of the processes.
     * @return average response time of all processes
     */
    public static double getAvgResponse()
    {
        double sum = 0;

        for (Process p : finishQ)
            sum += p.getResponseTime();

        return sum / finishQ.size();
    }

    /**
     * Activates a process to run.
     * @param time - absolute current time
     * @param gui - the panel containing the trace
     */
    public static void activate(double time, Display gui)
    {
        if (Processor.hasNoProcess()) {
            Processor.assignProcess(time, gui);
        }

        if (algorithm == Algorithm.PREEMPT &&
            readyQ.peek() != null &&
            !Processor.hasNoProcess() &&
            readyQ.peek().getPriority() < Processor.getCurrent().getPriority())
            swapProcesses(time, gui);

        Processor.run(time, gui);
    }
}

